﻿using System;
using System.IO;
using System.Net;
using System.Text;
using System.Xml;

namespace Toybox.CookieGetterSharp.Safari
{
  class SafariCookieGetter : CookieGetter
  {
    public SafariCookieGetter(CookieStatus status)
      : base(status)
    {
    }

    public override CookieContainer GetAllCookies()
    {
      if (base.CookiePath == null || !File.Exists(base.CookiePath))
      {
        throw new CookieGetterException("Safariのクッキーパスが正しく設定されていません。");
      }

      if (base.CookiePath.EndsWith("binarycookies"))
        return GetAllCookiesFromBinary();
      else
        return GetAllCookiesFromXml();
    }

    #region Binary(Safari5.1 Over)
    private CookieContainer GetAllCookiesFromBinary()
    {
      CookieContainer container = new CookieContainer();

      string temp = null;
      try
      {
        temp = Path.GetTempFileName();
        File.Copy(CookiePath, temp, true);

        using (var fs = new FileStream(temp, FileMode.Open))
        {
          int result;
          byte[] bytes;
          //int offset;

          // header
          bytes = new Byte[4];
          //bytes.Initialize();
          result = fs.Read(bytes, 0, bytes.Length);
          string header = Encoding.UTF8.GetString(bytes);

          // page number
          bytes.Initialize();
          result = fs.Read(bytes, 0, bytes.Length);
          Array.Reverse(bytes);
          int num = BitConverter.ToInt32(bytes, 0);

          // page length
          int[] lengthes = new int[num];
          for (int n = 0; n < num; n++)
          {
            bytes.Initialize();
            result = fs.Read(bytes, 0, bytes.Length);
            Array.Reverse(bytes);
            lengthes[n] = BitConverter.ToInt32(bytes, 0);
          }

          // page reader
          for (int n = 0; n < num; n++)
          {
            // page header
            bytes = new Byte[3];
            result = fs.Read(bytes, 0, bytes.Length);
            //Array.Reverse(bytes);

            // cookie number
            bytes = new Byte[2];
            result = fs.Read(bytes, 0, bytes.Length);
            Array.Reverse(bytes);
            int cnum = BitConverter.ToInt16(bytes, 0);

            // header length
            bytes = new Byte[4];
            result = fs.Read(bytes, 0, bytes.Length);
            Array.Reverse(bytes);
            int len = BitConverter.ToInt32(bytes, 0);

            // 破棄
            for (int m = len - 9; 0 < m; m--)
              fs.ReadByte();

            // Cookie取得
            for (int m = 0; m < cnum; m++)
            {
              Cookie cookie = getCookie(fs);
              try
              {
                Utility.AddCookieToContainer(container, cookie);
              }
              catch (Exception ex)
              {
                CookieGetter.Exceptions.Enqueue(ex);
                Console.WriteLine(string.Format("Invalid Format! domain:{0},key:{1},value:{2}", cookie.Domain, cookie.Name, cookie.Value));
              }
            }
          }
        }
      }
      catch (Exception ex)
      {
        throw new CookieGetterException("クッキーを取得中、binarycookiesでエラーが発生しました。", ex);
      }
      finally
      {
        if (temp != null)
        {
          File.Delete(temp);
        }
      }

      return container;

    }

    private Cookie getCookie(FileStream fs)
    {
      Byte[] bytes;
      int result;

      // クッキーの長さ
      bytes = new Byte[4];
      result = fs.Read(bytes, 0, bytes.Length);
      int clen = BitConverter.ToInt32(bytes, 0);

      // 破棄
      for (int l = 9 + 4 * 4 + 11; 0 < l; l--)
        fs.ReadByte();

      // 時間1
      bytes = new Byte[8];
      result = fs.Read(bytes, 0, bytes.Length);
      long time1 = BitConverter.ToInt64(bytes, 0);

      // 時間2
      bytes = new Byte[8];
      result = fs.Read(bytes, 0, bytes.Length);
      long time2 = BitConverter.ToInt64(bytes, 0);

      // 文字列
      bytes = new Byte[clen - (4 + 9 + 4 * 4 + 11 + 8 * 2)];
      result = fs.Read(bytes, 0, bytes.Length);
      string text = Encoding.UTF8.GetString(bytes);

      string[] elements = text.Split(new char[] { '\0' }, StringSplitOptions.RemoveEmptyEntries);

      return new System.Net.Cookie(elements[0], elements[1], elements[3], elements[2]);
      //return new System.Net.Cookie(elements[0], Uri.UnescapeDataString(Uri.UnescapeDataString(elements[1])), elements[3], elements[2]);
    }
    #endregion

    #region Xml(Safari5.0 Less)
    private CookieContainer GetAllCookiesFromXml()
    {
      CookieContainer container = new CookieContainer();

      try
      {
        XmlReaderSettings settings = new XmlReaderSettings();

        // DTDを取得するためにウェブアクセスするのを抑制する
        // (通信遅延や、アクセスエラーを排除するため)
        settings.XmlResolver = null;
        //settings.ProhibitDtd = false; // 旧スタイル
        settings.DtdProcessing = DtdProcessing.Ignore;
        settings.CheckCharacters = false;

        using (XmlReader xtr = XmlTextReader.Create(base.CookiePath, settings))
        {
          while (xtr.Read())
          {
            switch (xtr.NodeType)
            {
              case XmlNodeType.Element:
                if (xtr.Name.ToLower().Equals("dict"))
                {
                  Cookie cookie = getCookie(xtr);
                  try
                  {
                    Utility.AddCookieToContainer(container, cookie);
                  }
                  catch (Exception ex)
                  {
                    CookieGetter.Exceptions.Enqueue(ex);
                    Console.WriteLine(string.Format("Invalid Format! domain:{0},key:{1},value:{2}", cookie.Domain, cookie.Name, cookie.Value));
                  }
                }
                break;
            }
          }
        }

      }
      catch (Exception ex)
      {
        throw new CookieGetterException("Safariのクッキー取得中にエラーが発生しました。", ex);
      }

      return container;
    }

    private Cookie getCookie(XmlReader xtr)
    {
      bool isEnd = false;
      Cookie cookie = new Cookie();
      string tagName = "";
      string kind = "";

      while (xtr.Read() && !isEnd)
      {
        switch (xtr.NodeType)
        {
          case XmlNodeType.Element:
            tagName = xtr.Name.ToLower();
            break;

          case XmlNodeType.Text:
            switch (tagName)
            {
              case "key":
                kind = xtr.Value.ToLower();
                break;
              case "real":
              case "string":
              case "date":
                switch (kind)
                {
                  case "domain":
                    cookie.Domain = xtr.Value;
                    break;
                  case "name":
                    cookie.Name = xtr.Value;
                    break;
                  case "value":
                    cookie.Value = xtr.Value;
                    if (cookie.Value != null)
                    {
                      cookie.Value = Uri.EscapeDataString(cookie.Value);
                    }
                    break;
                  case "expires":
                    cookie.Expires = DateTime.Parse(xtr.Value);
                    break;
                  case "path":
                    cookie.Path = xtr.Value;
                    break;
                }
                break;
            }
            break;

          case XmlNodeType.EndElement:
            if (xtr.Name.ToLower() == "dict")
            {
              isEnd = true;
            }
            break;

        }
      }

      return cookie;
    }
    #endregion
  }
}
